Skip to content

Hotfix: dashboard Docker build needs @types/node#23

Closed
pulkitpareek18 wants to merge 13 commits into
mainfrom
dev
Closed

Hotfix: dashboard Docker build needs @types/node#23
pulkitpareek18 wants to merge 13 commits into
mainfrom
dev

Conversation

@pulkitpareek18
Copy link
Copy Markdown
Collaborator

The Deploy workflow on `main` failed at the Docker `dashboard-build` stage right after the dev→main merge in PR #22. Inside Docker the dashboard workspace is built in isolation (no parallel root `node_modules` to walk into), so `playwright.config.ts` and the test files lost their `@types/node` and tsc errored with `TS2591` / `TS2304`.

This PR adds `@types/node@^20` to the dashboard workspace and lists `node` in the dashboard tsconfig `types` array.

Verified:

  • `npm --prefix dashboard run typecheck` clean
  • `npm --prefix dashboard run build` produces a 330 KB / 98 KB gzipped bundle
  • `docker build --target dashboard-build` (isolated, no-cache) succeeds locally, reproducing the prod path

Why dev CI didn't catch this: the validate job runs both `npm ci`s in the same workspace and tsc walks up to find root's @types/node. Follow-up issue tracks adding a "build via Docker" check on dev too.

Live prod is currently still on the previous deployed image (the failed Docker build aborted before swapping the container).

ADR-0002 (new) documents why the developer console stays on Vite +
React + Tailwind + React Query rather than migrating to the suite's
Next.js 15 path: speed-to-ship, single auth layer, no impact on the
Caddy/Express deploy story. Names every new dep so the dep-trail
check can audit them.

CLAUDE.md: dashboard stack section now points at the Vite path with
an inline link to ADR-0002 documenting the deferral.

threat_model.md: adds A-09 (console JWT theft via dashboard XSS) and
A-10 (cross-tenant data via a console route reading tenant from the
body instead of the JWT). Both have explicit test-status rows so the
gaps are visible.

scripts/check-dep-trail.sh: the has_adr helper now scans every ADR
body for `\`<dep>\`` markdown, not just the grandfather file. Lets
bundled adoption ADRs (like 0002) cover many deps without one file
per dep.
These proxy endpoints back the developer console UI. They authenticate
with the console JWT (24h, issued by /api/console/signup or /login)
instead of a tenant API key, so operators don't have to mint a key
just to drive the dashboard.

All endpoints:
- read the tenant ID from `(req as any).console.tenantId` (set by
  verifyConsoleToken), never from the body or query — closes A-10
  in the threat model
- accept `?environment=live|test` from the query, defaulting to live
- delegate to the existing platform service so business rules and
  audit-log side effects are identical to the /v1/* tenant-API-key
  paths

Endpoints added:
- GET    /api/console/devices               (filter by status, limit)
- POST   /api/console/devices               (validates batteryLevel)
- PATCH  /api/console/devices/:id
- GET    /api/console/users                 (filter by status, limit)
- POST   /api/console/users
- PATCH  /api/console/users/:id
- GET    /api/console/verifications         (filter by method, result)
- GET    /api/console/attendance            (filter by type, result)

tests/console-proxy.test.ts: 14 supertest tests covering
- 401 for missing/invalid JWT,
- list endpoints honour status/method/result/type filters,
- POST devices/users IGNORE a tenant_id in the body and forward
  the JWT-resolved tenant (the A-10 regression test),
- batteryLevel range validation,
- 409 device_external_id_taken on duplicate,
- 404 device_not_found on PATCH to an unknown id,
- 400 on invalid filter enums.

Full root jest now: 64 tests across 10 suites (was 50 / 9).
Replaces the 520-line single-file admin-stats viewer with a real
tenant-scoped console. Stack per ADR-0002: Vite 7 + React 19 +
TypeScript strict + React Router 7 + TanStack Query 5 + Tailwind
CSS 4 + vitest + RTL + ESLint 9 flat config.

Pages (under /dashboard, basename-routed)
- /login                — email + password, redirects to where the
                          user came from on success
- /signup               — 12+ char password policy mirrored from
                          the API; first API key revealed once with
                          a confirmation gate before navigation
- /overview             — counts, recent verifications, recent
                          audit, usage-this-month with quota bar,
                          getting-started checklist, last 25 API calls
- /api-keys             — list with scopes/env/last-used, create
                          modal (scope checkboxes, env selector,
                          one-time reveal), revoke confirmation
- /users                — list with status filter + enroll modal
- /devices              — list with status filter + register modal
                          (battery 0–100 validation)
- /verifications        — read-only, filter by method + result
- /attendance           — read-only, filter by type + result
- /audit                — append-only feed with action substring +
                          status filter
- /settings             — account info, plan + limits, danger zone
                          stub (email security@zeroauth.dev to
                          suspend / delete; no self-service yet)
- 404                   — back-to-overview link

Library
- src/lib/api.ts        — typed fetch wrapper. JWT in localStorage,
                          attached as Bearer on every authed request.
                          401 from /api/console/* purges the token so
                          the next render bounces to /login.
- src/lib/auth.tsx      — AuthProvider, useAuth, status machine
                          (loading | authenticated | unauthenticated)
- src/lib/format.ts     — number/relative/datetime/ms/truncate helpers
- src/lib/cn.ts         — clsx wrapper

Layout
- AppShell              — sidebar + topbar + outlet, environment
                          switcher (live/test) persisted in
                          localStorage, mobile drawer, sign-out
- RequireAuth           — router guard, redirects to /login while
                          preserving `from` for post-login bounceback

UI primitives (hand-written; no shadcn / no radix)
- Button (4 variants, 3 sizes, loading spinner)
- Input / Textarea / Select / Label
- Card / CardHeader / CardBody
- Badge (5 tones)
- Skeleton, EmptyState
- Modal (Escape closes, body-scroll lock, dialog ARIA)
- Toast (subscribable, dismiss on click, 4s ttl)
- CopyButton (clipboard fallback toast)

Tests (vitest + @testing-library/react + jsdom — 18/18 passing)
- lib/api.test.ts (5)        — Bearer attach, no-auth on signup/
                               login, ApiError shape, 401 purges
                               token, query serialisation
- lib/format.test.ts (5)     — number/compact/ms/relative/truncate
- components/ui/Button.test  — click, disabled-while-loading,
                               variant classes
- components/ui/Modal.test   — open/close, Escape, ARIA role
- routes/public/Login.test   — form render, 401 inline error,
                               successful login redirects via the
                               mocked /api/console/account fetch

Build: tsc --noEmit + vite build produce a 330 KB JS bundle
(98 KB gzipped), 30 KB CSS (5.75 KB gzipped). Source maps emitted.

Old files removed: src/App.tsx (520 lines), src/hooks/*, vite-env.d.ts.
ci.yml now triggers on push to main AND dev, so the working branch
gets the same gating as production. Adds three dashboard checks
(typecheck, lint, test) plus an advisory dep-trail audit so DP6
violations show up on every PR.

PRs from dev → main continue to fire via `pull_request:`, so we get
two gates: one on every dev push, one when the PR opens.
Adds the first end-to-end test exercising the dashboard against a
real Express + Postgres backend, plus the CI plumbing to run it on
every PR / push to main and dev.

ADR-0003 documents the adoption choice (Playwright over Cypress /
Selenium / no-E2E), the operational expectations, and the rationale
for chromium-only at this stage.

Test (dashboard/e2e/happy-path.spec.ts)
- /dashboard/signup → fill 12-char password + company → submit
- Assert the one-time API key reveal modal contains a
  za_(live|test)_<48 hex> string
- Tick the "I've saved this key" confirmation → continue to Overview
- Assert sidebar reflects the new tenant identity
- Navigate to API Keys → assert the default key row is present
- Mint a second key (test env) → confirm + dismiss reveal modal
- Assert the new key row shows the test badge
- Switch env switcher to "test"
- Navigate to Devices → register a device with battery=87 →
  assert toast + row appear
- Navigate to Audit → toggle env to verify tenant.created (live)
  and device.created (test) rows are both present
- Sign out → land on /dashboard/login

Playwright config (dashboard/playwright.config.ts)
- baseURL from E2E_BASE_URL env (defaults http://localhost:3000)
- fullyParallel: false, workers: 1 — signup is sequential
- retries: 2 in CI, 0 locally
- trace on first retry, screenshot on failure, video retain-on-failure
- reporter: list + html-no-open in CI; list locally
- chromium-only project (Firefox/WebKit additions are cheap later)

dashboard package.json
- new scripts: e2e, e2e:install (--with-deps chromium), e2e:ui

CI (.github/workflows/ci.yml)
- New `e2e` job (`needs: validate`) so it only runs after the existing
  lint + typecheck + tests + build pass
- Postgres 16 service container (zeroauth_e2e DB), 5432 → 5432
- Env: NODE_ENV=production, ENABLE_DEMO_AUTH=false, mocked secrets,
  POSTGRES_* pointing at the service container, E2E_BASE_URL=http://localhost:3000
- Steps: install root + dashboard + website deps → build:all →
  cache + install chromium → start `node dist/server.js` in
  background → wait for /api/health → run `npm --prefix dashboard run e2e`
  → kill the server in `if: always`
- Uploads server.log on failure + the Playwright HTML report
  (always, 14d retention)

Gitignore: ignores dashboard/playwright-report/, test-results/,
.playwright/ so traces + report artifacts stay out of git.

Local DX: `./scripts/deploy.sh dev` (postgres + redis + app on
:3000), then `cd dashboard && npm run e2e`. UI mode for stepping
through failures: `npm --prefix dashboard run e2e:ui`.

Backend was already verified clean (64 tests across 10 suites);
dashboard unit suite (18 tests) is unchanged. CI on push will be
the source of truth for the E2E result on this commit.
The previous commit added dashboard/e2e/happy-path.spec.ts but didn't
narrow vitest's default `**/*.{test,spec}.?(c|m)[jt]s?(x)` discovery,
so vitest tried to import the Playwright spec — which uses a different
test/expect API — and the dashboard test step failed in CI.

vite.config.ts now sets explicit include/exclude on the test config:
- include: src/**/*.{test,spec}.{ts,tsx}
- exclude: e2e/, playwright-report/, test-results/ (plus node_modules/dist)
- coverage.exclude mirrors the same e2e/ ignore

Local re-run: 18/18 vitest tests pass. The Playwright spec is still
listed by `npx playwright test --list` and is exercised by the new
`e2e` CI job, just not by vitest.
The implicit "node" value normalises to "node10" internally, but TS 6.x
treats that as a hard error TS5107 ("deprecated and will stop functioning
in TypeScript 7.0"). The e2e job's runner picked up TS 6.x via npm's
resolution cache while the validate job, on the same commit, got TS 5.9
and passed. Pinning the explicit non-deprecated name "node10" gives the
same behaviour in TS 5.x AND TS 6.x.

Local verify: tsc --noEmit clean, build:all clean (backend + dashboard
+ docs).
The previous fix to "node10" was still flagged as deprecated in
whatever TypeScript the CI e2e job is resolving. Node16 is the
unambiguous non-deprecated value supported in TS 5.x and 6.x.

module must be paired with moduleResolution per TS rules — both
flipped to "Node16". Local tsc --noEmit clean, npm run build:all
clean, 64/64 backend tests pass. The runtime emit stays effectively
CommonJS because package.json has no `"type": "module"`, so no
import sites need .js extensions added.
Reverts the Node16 attempt which broke @types/* discovery — Node16
resolution doesn't auto-pick up types from node_modules/@types the
same way the node resolver does, so the backend lost @types/uuid,
@types/pg, @types/jsonwebtoken, @types/express.

Back to the proven setup:
  module: commonjs
  moduleResolution: node

with the explicit ignoreDeprecations: "5.0" flag so the TS5107
deprecation message ("Option 'moduleResolution=node10' is deprecated")
doesn't fail the build. The flag is a no-op on older TS, and stays
green until we migrate to Node16 + explicit @types listing in some
later, dedicated PR.

Local tsc --noEmit + build:all both clean.
Both CI jobs run the same `npm run build:all` against the same
lockfile, but validate consistently passes and e2e consistently
fails with TS5107 demanding `ignoreDeprecations: "6.0"` instead of
the "5.0" my locked TS 5.9.3 expects. The lockfile pins TS to
5.9.3 in exactly one place, so npm ci should produce the same
node_modules/typescript across both jobs.

Adds a diagnostic step before "Build everything" that prints:
- which tsc + npx tsc --version
- node_modules/typescript/package.json version
- TS api version

so the next run gives us the actual installed version.

Also drops `typeRoots` from tsconfig — the default behaviour (auto-
include @types/* from node_modules/@types) is what we want, and the
explicit typeRoots may have been masking a different resolution
quirk in some TS versions.
The e2e job set NODE_ENV=production as a job-level env var so the
backend would behave like prod (demo-auth gate firing, etc.). That
also made every preceding `npm ci` skip devDependencies, including
typescript, vitest, eslint, @types/*, vite, etc. Then `npm run build`
couldn't find a local tsc and resolved /usr/local/bin/tsc on the
runner — which turned out to be the bogus `tsc@2.0.4` npm package,
which printed:

  This is not the tsc command you are looking for

Resulting in TS5107-style errors from a completely different binary
than what we run locally. That explains why validate (no NODE_ENV)
succeeded with TS 5.9.3 while e2e (NODE_ENV=production) "failed with
TS 6.x" — there was no TS 6.x, the runner was running an entirely
different impostor.

Fix: move NODE_ENV + every runtime secret to the "Start backend"
step only. Install / build steps run with the default ubuntu-latest
env so devDependencies install normally.

Side cleanups:
- removed the temporary "Diagnose TypeScript resolution" CI step (its
  purpose served — caught the impostor tsc)
- reverted tsconfig.json to the original commonjs/node setup (no
  ignoreDeprecations needed once the right tsc is running)

Local tsc --noEmit + build:all clean.
The CI showed the test getting all the way through signup → first-
key reveal → mint a second key → reveal → list. It then failed at
the env-badge in-row check because getByText('test') ambiguously
matched BOTH the za_test_<hex> prefix cell AND the badge span in
strict mode.

Fixes:
- env badge: scope to span inside the row + exact regex match
- audit log: simplify to a single "test env shows device.created"
  assertion with a 15s timeout, since recordAuditEvent is fire-and-
  forget and the live/test env-switching dance was racy.

Local typecheck + lint pass.
The Docker `dashboard-build` stage installs ONLY the dashboard
workspace's deps and runs tsc --noEmit. Locally and in CI's validate
job the dashboard tsc walked UP from dashboard/node_modules to find
the root's @types/node — both workspaces share a parent filesystem.
Inside Docker, /app/dashboard/node_modules is in isolation and there
is no /app/node_modules to walk into, so playwright.config.ts and
the test files erupted with TS2591 ("Cannot find name 'process'")
and TS2304 ("Cannot find name 'global'").

The production deploy on main consequently failed at this exact step
right after the dev → main merge fired the Deploy workflow.

Fix:
- Add @types/node ^20 to dashboard devDependencies (matches the root
  pin so the SDK version is identical).
- Add "node" to dashboard/tsconfig.json compilerOptions.types so the
  explicit types list activates the new install.

Verified locally:
- npm --prefix dashboard run typecheck  → clean
- npm --prefix dashboard run build      → 330 KB / 98 KB gzipped, source maps emitted
- docker build --target dashboard-build → succeeds in isolation,
  reproducing the prod deploy path

The dev branch's CI did not catch this because validate runs both
root + dashboard `npm ci` in the same workspace, so root's
@types/node was always reachable. Long-term gap: add a "build via
Docker" check on dev too. Tracked separately.
Copilot AI review requested due to automatic review settings May 12, 2026 11:36
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to fix isolated Docker builds of the dashboard workspace by ensuring Node.js types are available during TypeScript compilation. In practice, it also introduces a much broader set of changes: new console proxy API endpoints, a full dashboard SPA rewrite/structure, Playwright E2E coverage, and CI workflow expansion.

Changes:

  • Add @types/node to the dashboard workspace and explicitly include node in dashboard/tsconfig.json types.
  • Add/extend console proxy endpoints under /api/console/* and introduce integration tests to verify tenant scoping.
  • Expand the dashboard toolchain (Tailwind, vitest, Playwright) and update CI to run dashboard checks + E2E.

Reviewed changes

Copilot reviewed 43 out of 46 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/console-proxy.test.ts Adds integration coverage for /api/console/* proxy behavior and tenant scoping.
src/routes/console.ts Adds console proxy endpoints for devices/users/verifications/attendance.
scripts/check-dep-trail.sh Broadens ADR detection to allow bundled ADRs to satisfy dependency audit.
docs/threat_model.md Adds/updates threats A-09/A-10 related to dashboard XSS/JWT and cross-tenant access.
dashboard/vite.config.ts Adds Tailwind plugin, sourcemaps, vitest configuration, and additional dev proxies.
dashboard/tsconfig.json Updates TS target/libs and adds explicit types + broader include.
dashboard/src/vite-env.d.ts Removes Vite client type reference file.
dashboard/src/test/setup.ts Adds vitest/RTL setup and a matchMedia shim for jsdom.
dashboard/src/styles.css Introduces Tailwind v4 CSS-first theme tokens and global styles.
dashboard/src/routes/Verifications.tsx Adds Verifications page with filters and table rendering.
dashboard/src/routes/Users.tsx Adds Users page and “Enroll user” modal flow.
dashboard/src/routes/Settings.tsx Adds Settings page showing account and plan/usage details.
dashboard/src/routes/public/Signup.tsx Adds signup flow with first API key reveal modal.
dashboard/src/routes/public/Login.tsx Adds login flow and shared public auth layout.
dashboard/src/routes/public/Login.test.tsx Adds component test coverage for login rendering, errors, and redirect.
dashboard/src/routes/Overview.tsx Adds Overview page with stats, recent activity, and getting-started checklist.
dashboard/src/routes/NotFound.tsx Adds a dashboard 404 page.
dashboard/src/routes/Devices.tsx Adds Devices page and “Register device” modal flow.
dashboard/src/routes/Audit.tsx Adds Audit Log page with filtering UI.
dashboard/src/routes/Attendance.tsx Adds Attendance page with type/result filters and table rendering.
dashboard/src/routes/ApiKeys.tsx Adds API key management UI (create/revoke) with one-time reveal modal.
dashboard/src/main.tsx Updates React entrypoint and wires global styles import.
dashboard/src/lib/format.ts Adds formatting helpers used across the dashboard.
dashboard/src/lib/format.test.ts Adds unit tests for dashboard formatting helpers.
dashboard/src/lib/cn.ts Adds clsx-based className helper.
dashboard/src/lib/auth.tsx Adds auth context/provider for console JWT session management.
dashboard/src/lib/api.ts Adds typed fetch client for console endpoints + token storage behavior.
dashboard/src/lib/api.test.ts Adds unit tests for API client header/query/error/token behaviors.
dashboard/src/hooks/useStats.ts Removes legacy admin stats hook.
dashboard/src/hooks/useLeads.ts Removes legacy leads hook.
dashboard/src/hooks/useBlockchain.ts Removes legacy blockchain hook.
dashboard/src/components/ui/index.tsx Adds hand-written UI primitives (Button/Input/Card/Modal/Toast/etc.).
dashboard/src/components/ui/Button.test.tsx Adds tests for Button/Modal primitives.
dashboard/src/components/layout/AppShell.tsx Adds dashboard shell layout, navigation, and env switcher.
dashboard/src/App.tsx Replaces legacy dashboard app with router-based SPA composition and guards.
dashboard/playwright.config.ts Adds Playwright test configuration for E2E suite.
dashboard/package.json Expands dashboard deps/scripts (typecheck/lint/vitest/Playwright/Tailwind) and adds @types/node.
dashboard/index.html Updates dashboard HTML shell and fonts/meta; aligns for new SPA styling.
dashboard/eslint.config.js Adds dashboard-specific ESLint flat config.
dashboard/e2e/happy-path.spec.ts Adds Playwright happy-path E2E spec covering signup → key → device → audit.
CLAUDE.md Updates repo documentation to reflect the dashboard stack and tooling.
adr/0003-adopt-playwright-for-e2e.md Adds ADR documenting Playwright adoption for dashboard E2E.
adr/0002-dashboard-stack-vite-not-nextjs.md Adds ADR documenting Vite SPA decision vs Next.js.
.gitignore Ignores Playwright outputs under dashboard/.
.github/workflows/ci.yml Expands CI to run dashboard checks and adds a Playwright E2E job with Postgres service.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/routes/console.ts
Comment on lines +415 to +423
// ─── Console proxy endpoints for the platform domain ──────────────
//
// These exist so the dashboard can manage devices, users, verifications,
// and attendance using the console JWT — without forcing the operator to
// mint a tenant API key. They are thin wrappers over `platform.ts` that
// resolve the tenant from the JWT, accept `environment=live|test` from
// the query (defaulting to live), and pass `actorId=null` since these are
// operator actions (no api_key_id; audit rows record `actor_type=console`).

Comment on lines +79 to +84
const location = useLocation();
const [mobileOpen, setMobileOpen] = useState(false);

// Close the mobile sidebar after every navigation.
useState(() => location.pathname);

Comment thread docs/threat_model.md
Comment on lines +105 to +107
| **Description** | The console JWT lives in client memory and is replayed on every API call. If an XSS payload executes in the SPA, the attacker reads the token from memory and uses it from anywhere. |
| **Mitigation** | (a) Strict CSP from Helmet — no `unsafe-eval`, no inline scripts beyond the existing landing-page allowance. (b) React's default escape protects against most reflected XSS. (c) **Never** introduce `dangerouslySetInnerHTML` without an ADR. (d) The console JWT is short-lived (24h) and revocable by tenant suspension. |
| **Test status** | CSP header presence is asserted in `tests/health.test.ts` (indirectly via helmet output). **Missing:** an integration test that asserts no inline `<script>` blocks land in the dashboard build output and an integration test for `dangerouslySetInnerHTML` absence. |
Comment thread dashboard/package.json
Comment on lines 1 to +17
{
"name": "zeroauth-dashboard",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"build": "tsc --noEmit && vite build",
"preview": "vite preview",
"lint": "eslint src",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"e2e": "playwright test",
"e2e:install": "playwright install --with-deps chromium",
"e2e:ui": "playwright test --ui"
@pulkitpareek18
Copy link
Copy Markdown
Collaborator Author

Superseded by hotfix-typesnode → main (PR coming). dev had non-cherrypickable squash conflicts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants